A modern real estate platform built with Next.js, PocketBase, and deployed on Cloudflare Workers using OpenNext.
A modern real estate platform built with Next.js and deployed on Cloudflare Workers.
Live demo: earth-and-home-nextjs.denniskinuthiaw.workers.dev
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack, React Compiler) |
| Language | TypeScript (strict) |
| UI | Tailwind CSS v4 + DaisyUI v5 + shadcn/ui |
| Database | Cloudflare D1 (SQLite) via Drizzle ORM |
| Auth | Better Auth (Google OAuth) |
| Storage | Cloudflare R2 (media) + Cloudflare Images |
| Cache | R2 Incremental Cache with Regional Cache |
| State | TanStack Query |
| Deployment | Cloudflare Workers via OpenNext |
pnpm add -g wrangler)wrangler login)pnpm install
Create a .env file:
NODE_ENV=development
BETTER_AUTH_URL=http://localhost:3010
NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3010
BETTER_AUTH_SECRET=<generate-a-random-secret>
GOOGLE_CLIENT_ID=<your-google-oauth-client-id>
GOOGLE_CLIENT_SECRET=<your-google-oauth-client-secret>
pnpm db:migrate:local
pnpm dev
Open http://localhost:3010.
| Script | Description |
|---|---|
pnpm dev |
Start dev server (Turbopack, port 3010). Runs local D1 migrations automatically. |
pnpm build |
Production build |
pnpm lint |
Lint with oxlint |
pnpm format |
Format with oxfmt |
| Script | Description |
|---|---|
pnpm db:generate |
Generate Drizzle migration files from schema changes |
pnpm db:migrate:local |
Apply migrations to local D1 |
pnpm db:migrate:remote |
Apply migrations to production D1 |
pnpm db:studio |
Open Drizzle Studio for local DB |
pnpm auth:generate |
Regenerate Better Auth schema after plugin changes |
| Script | Description |
|---|---|
pnpm run preview |
Build and preview locally in Workers runtime |
pnpm run deploy |
Build and deploy to Cloudflare Workers |
pnpm run upload |
Build and upload without making live |
pnpm run cf-typegen |
Generate cloudflare-env.d.ts types |
| Script | Description |
|---|---|
pnpm run analyze |
Bundle size analysis |
pnpm run lighthouse |
Lighthouse audit against localhost |
The app requires these Cloudflare resources (configured in wrangler.jsonc):
| Resource | Binding | Name |
|---|---|---|
| D1 Database | DB |
earth-and-home-db |
| R2 Bucket (cache) | NEXT_INC_CACHE_R2_BUCKET |
earth-and-home-nextjs-opennext-cache |
| R2 Bucket (media) | MEDIA |
earth-and-home-media |
| Cloudflare Images | IMAGES |
— |
| Worker Self-Reference | WORKER_SELF_REFERENCE |
earth-and-home-nextjs |
Create them if deploying to your own account:
wrangler d1 create earth-and-home-db
wrangler r2 bucket create earth-and-home-nextjs-opennext-cache
wrangler r2 bucket create earth-and-home-media
Update the database_id in wrangler.jsonc with the ID returned by the D1 create command.
Build-time public variables go in .env.production:
NEXT_PUBLIC_BETTER_AUTH_URL=https://your-worker.workers.dev
NEXT_PUBLIC_* variables are inlined at build time by Next.js. They do not work as Cloudflare runtime secrets.
Runtime secrets are set via Wrangler CLI:
wrangler secret put BETTER_AUTH_SECRET
wrangler secret put BETTER_AUTH_URL
wrangler secret put GOOGLE_CLIENT_ID
wrangler secret put GOOGLE_CLIENT_SECRET
# Apply any pending D1 migrations to production
pnpm db:migrate:remote
# Build and deploy
pnpm run deploy
The deploy script automatically stubs the @vercel/og WASM file (~2 MB) to stay within the 3 MB Worker size limit, then restores it after upload.
NEXT_PUBLIC_BETTER_AUTH_URL in .env.production and BETTER_AUTH_URL secret to matchhljs ├── drizzle/ # D1 migration files
├── scripts/
│ ├── set-production-secrets.js # Bulk secret management
│ ├── list-production-secrets.js # List configured secrets
│ └── stub-resvg-wasm.sh # WASM stub for bundle size
├── src/
│ ├── app/ # Next.js App Router pages & API routes
│ ├── components/
│ │ ├── common/ # Reusable UI components
│ │ ├── dashboard/ # Dashboard feature components
│ │ ├── theme/ # Theme provider & toggle
│ │ └── ui/ # shadcn/ui primitives
│ ├── config/ # Site configuration
│ ├── data-access-layer/ # Server-side data fetching
│ ├── db/
│ │ └── schema/ # Drizzle schema definitions
│ ├── hooks/ # React hooks
│ ├── lib/
│ │ ├── auth/ # Better Auth client & server
│ │ ├── db/ # D1 database connection
│ │ └── tanstack/ # TanStack Query setup
│ ├── services/ # Domain-specific API services
│ └── types/ # TypeScript type definitions
├── middleware.ts # Edge middleware (auth guard)
├── open-next.config.ts # OpenNext caching config
├── wrangler.jsonc # Cloudflare Worker config
└── drizzle.config.ts # Drizzle Kit config (local D1)
The app uses R2 Incremental Cache with Regional Cache (open-next.config.ts):
Static assets are cached via public/_headers (1-year immutable for JS/CSS/fonts, 30 days for images).
Better Auth handles authentication with Google OAuth. The Edge middleware (middleware.ts) protects /dashboard/* routes by checking session cookies. In production over HTTPS, cookies are automatically prefixed with __Secure- — the middleware handles both prefixed and non-prefixed names.